好的,接下來我們要新增邀約的Fragment,好讓使用者可以上去PO出自己的邀約,以及讓不同的使用者可以看到目前有的邀約。那我們開始吧! 今天會完成上傳圖片/選單
頁面長相長這樣!
data class Invitation(
val id: String = "",
val user_id: String = "",
val pet_image: String = "",
val pet_type: String = "",
val pet_type_description: String = "",
val area: String = "",
val date_place: String = "",
val date_time: String = "",
val note: String = ""
)
一樣繼承 BaseFragment以及databinding
<dimen name="image_height">250dp</dimen>
<dimen name="icon_camera_margin_bottom_end">10dp</dimen>
<dimen name="add_invitation_input_layout_margin_top_bottom"
<string name="toolbar_title_add_invitation">新增邀約</string>
<string name="hint_enter_pet_type">請選擇寵物種類</string>
<string name="hint_enter_pet_type_description">請輸入寵物品種</string>
<string name="hint_enter_date_place">請輸入邀約地點</string>
<string name="hint_enter_date_time">請輸入邀約時間</string>
<string name="hint_enter_date_note">請輸入注意事項(如寵物害怕物品..)</string>
<string name="update_pet_image_successful">上傳圖片成功!</string>
<string name="hint_enter_area">請輸入邀約區域</string>
為了達到視覺效果統一,layout大部分會是用material.textfield.TextInputLayout來包。然後因為這次的填寫的內容比較多,所以我們會用Scroll View來包。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragment.AddInvitationFragment">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_add_invitation_fragment"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/light_pewter_blue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/toolbar_title_add_invitation"
android:textColor="@color/white"
android:textSize="@dimen/toolbar_textSize"
android:textStyle="bold">
</TextView>
</androidx.appcompat.widget.Toolbar>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_add_invitation_fragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<FrameLayout
android:id="@+id/fl_add_invitation_image"
android:layout_width="match_parent"
android:layout_height="@dimen/image_height"
android:background="@color/grey_light"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_add_invitation_pet_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY">
</ImageView>
<ImageView
android:id="@+id/iv_add_invitation_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/icon_camera_margin_bottom_end"
android:layout_marginBottom="@dimen/icon_camera_margin_bottom_end"
android:src="@drawable/ic_baseline_photo_camera_24">
</ImageView>
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_add_invitation_fragment_spinner_pet_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/tip_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/fl_add_invitation_image"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/spinner_pet_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:hint="@string/hint_enter_pet_type"
android:textSize="@dimen/edText_textSize"
android:padding="@dimen/edText_padding"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_add_invitation_fragment_pet_type_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/add_invitation_input_layout_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_spinner_pet_type"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_add_invitation_pet_type_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"
android:hint="@string/hint_enter_pet_type_description"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_add_invitation_fragment_spinner_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/add_invitation_input_layout_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_pet_type_description"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu">
<AutoCompleteTextView
android:id="@+id/spinner_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:hint="@string/hint_enter_area"
android:textSize="@dimen/edText_textSize"
android:padding="@dimen/edText_padding"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_add_invitation_fragment_date_place"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/add_invitation_input_layout_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_spinner_area"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_add_invitation_date_place"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"
android:hint="@string/hint_enter_date_place"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_add_invitation_fragment_date_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/add_invitation_input_layout_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_date_place"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_add_invitation_date_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"
android:hint="@string/hint_enter_date_time"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tip_add_invitation_fragment_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:layout_marginTop="@dimen/add_invitation_input_layout_margin_top_bottom"
app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_date_time"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.example.petsmatchingapp.utils.JFEditText
android:id="@+id/ed_add_invitation_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:textSize="@dimen/edText_textSize"
android:hint="@string/hint_enter_date_note"/>
</com.google.android.material.textfield.TextInputLayout>
<com.example.petsmatchingapp.utils.JFButton
android:id="@+id/btn_add_invitation_fragment_submit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tip_add_invitation_fragment_note"
android:layout_marginTop="@dimen/button_margin_top_bottom"
android:layout_marginStart="@dimen/tip_margin_start_end"
android:layout_marginEnd="@dimen/tip_margin_start_end"
android:background="@drawable/button_background"
android:foreground="?attr/selectableItemBackground"
android:textColor="@color/white"
android:text="@string/submit"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
知道我們架構的小夥伴們都知道,我們主要有分兩個viewModel,一個是負責我們的帳號相關的AccountViewModel,另外一個則是主要放Mathcing資料的viewModel,所以我們接下來前置工作就是要新增viewModel,跟設定koin
一樣先建立MatchingViewModel,並繼承viewModel,別忘了去MyApp的startKoin funtion新增
val viewModelModule = module {
viewModel { AccountViewModel() }
//新增這個
viewModel { MatchingViewModel() }
}
雖然權限我們之前在ProfileFragment設定過了,但是也有可能那時候使用者拒絕,所以我們在這邊還要再問一次!
private fun checkPermission(){
//確認是否有權限,如果有權限則開啟相簿
if (ContextCompat.checkSelfPermission(requireActivity(),android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
resultLauncher.launch(
Intent(Intent.ACTION_PICK,MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
)
}else{
//沒有權限則要求權限
requestPermission()
}
}
這邊出現紅字,我們來新增要求權限
private fun requestPermission(){
ActivityCompat.requestPermissions(requireActivity(), arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),Constant.REQUEST_CODE_READ)
}
如果沒有跟到前面幾個文章的小夥伴們,也要記得在manifest新增權限喔!
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ uri ->
if (uri.resultCode == Activity.RESULT_OK){
//一樣透過 data.data去拿uri
val selectedUri = uri.data?.data
if (selectedUri != null){
//有result的時候,就先把他用Glide顯示進去。
Constant.loadPetImage(selectedUri,binding.ivAddInvitationPetImage)
//再來傳去storage
matchingViewModel.saveImageToFireStorage(requireActivity(),this,selectedUri)
}
}
}
因為我們在loadUserImage的時候,我們為了讓層次更加明顯,以及筆者在做update使用者圖片時,剛好是中秋節,為了讓人比月嬌,所以我們把它改成圓角,但是寵物就不需要了,我們希望寵物邀約的時候,能更無死角,更全面性的看,所以我們要修改一下 Glide的地方,一樣去Constant新增以下
我們只要把圓角拿掉就好
fun loadPetImage(url: Any, v:ImageView){
Glide.with(v)
.load(url)
.placeholder(R.drawable.placeholder)
.into(v)
}
別忘了要先宣告matchingViewModel喔!
我們在傳送邀約的時候,也會需要po出這個訊息的人的ID(這也是為什麼Invitation這個dataClass會需要有 user_id的資訊),這樣我們對這個邀約的操控性就可以更高,譬如說之後我們可能不希望在所有約散的Fragment時,看到自己的PO出的訊息等...
private val matchingViewModel: MatchingViewModel by sharedViewModel()
private val accountViewModel: AccountViewModel by sharedViewModel()
到 MatchingViewModel新增上傳到 Storage的funtion啦!
原則上跟上傳user的頭貼一樣,我們只要把child名稱改變就好,以便之後我們好辨識
//傳入的fragment就要改成AddInvitationFragment
fun saveImageToFireStorage(activity: Activity, fragment: AddInvitationFragment, uri: Uri){
val sdf: StorageReference = FirebaseStorage.getInstance().reference.child(Constant.PET_IMAGE + "_" + System.currentTimeMillis() + "_" + Constant.getFileExtension(activity, uri))
sdf.putFile(uri)
.addOnSuccessListener {
//當上傳資料成功時,我們在新增Successful的listener,並且把它改成 downloadUri,並傳回fragment!
it.metadata?.reference?.downloadUrl
?.addOnSuccessListener {
fragment.saveImageSuccessful(it)
}
?.addOnFailureListener {
fragment.saveImageFail(it.toString())
}
}
.addOnFailureListener {
fragment.saveImageFail(it.toString())
}
}
接下來我們發現紅字,我們來解決它,一樣回Fragment來新增以下,且因為我們要讓我們的 mUri 存活在整個class,所以我們在class新增一個 null的 mUri
private var mUri: String? = null
fun saveImageSuccessful(uri: Uri){
mUri = uri.toString()
}
fun saveImageFail(e:String){
showSnackBar(e,true)
}
一樣先繼承 View.OnClickListener,並新增check權限的funtion
override fun onClick(v: View?) {
when(v){
binding.ivAddInvitationCamera -> {
checkPermission()
}
}
}
別忘了在 onCreateView新增
binding.ivAddInvitationCamera.setOnClickListener(this)
我們這邊一樣透過外面在包一個 inputLayout,這樣就可以保證我們的layout長相都一樣。其中也有很多有趣的設計,可以看一下 https://material.io/components/text-fields/android
我們先做一個 String List,先在class 先延遲初始化
private lateinit var petList: List<String>
private lateinit var areaList: List<String>
再來onCreateView賦值
petList = listOf("狗", "貓", "兔子", "鳥","豬","魚","其它")
areaList = listOf("基隆市","台北市","新北市","桃園市","新竹市","新竹縣","苗栗縣","彰化縣","雲林縣","南投縣","台中市","嘉義市","嘉義縣","台南市",
"高雄市","屏東縣","宜蘭縣","花蓮縣","台東縣","澎湖縣","金門縣","連江縣","其它")
★貼心小提醒,選單要記得添加其它呦,要不然沒有看到適合自己的人就會覺得被排擠!!
private fun setSpinner(){
val petAdapter = ArrayAdapter(requireContext(),R.layout.spinner_list_item,petList)
binding.spinnerPetType.setAdapter(petAdapter)
val areaAdapter = ArrayAdapter(requireContext(),R.layout.spinner_list_item,areaList)
binding.spinnerArea.setAdapter(areaAdapter)
}
ArrayAdapter傳入
再來新增 layout,並命名為 spinner_list_item
<?xml version="1.0" encoding="utf-8"?>
<TextView app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/edText_padding"
android:ellipsize="end"
android:maxLines="1"
android:textSize="@dimen/edText_textSize"
android:textAppearance="?attr/textAppearanceSubtitle1"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" />
★注意外面不用包layout喔,自己在做的時候就有手濺,自己亂加,結果就導致不行
取得資料
我們先在class下新增
private var selectedPetType: String? = null
private var selectedArea: String? = null
新增setOnItemClickListener就可以拿到 view,position,id 等資料囉! 但我們這邊只要 position就好了!
binding.spinnerPetType.setOnItemClickListener { _, _, position, _ ->
selectedPetType = petList[position]
}
binding.spinnerArea.setOnItemClickListener { _, _, position, _ ->
selectedArea = areaList[position]
}
好的,今天就先告一段落啦!! 目前還沒有弄Navigation導航到這個Fragment,我相信有認真學習的夥伴們一定可以自己做出來,所以我們就先給大家看一下小成果吧!!
大家也可以自己Log看看有沒有成功拿到資料!